﻿using System;
using System.Data;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Configuration;

namespace BizElements.Core
{
    /// <summary>
    /// Represents AND and OR operators that link the items in the SearchCondition.
    /// </summary>
    [Serializable]
    internal struct ConditionOperator : IRenderSql
    {
        #region Predefined operators: AND, OR.

        /// <summary>AND operator.</summary>
        public static readonly ConditionOperator And = new ConditionOperator("AND");

        /// <summary>OR operator.</summary>
        public static ConditionOperator Or = new ConditionOperator("OR");

        #endregion

        #region Private CTor and fields. Only predefined object may be used.

        /// <summary>Operator. 'AND' or 'OR'.</summary>
        string op;

        /// <summary>Initilizes operator.</summary>
        /// <param name="op">Operator. 'AND' or 'OR'.</param>
        private ConditionOperator(string op)
        {
            this.op = op;
        }

        #endregion

        #region IRenderSql.

        /// <summary>
        /// Renders operator as SQL statement. Space is appended before and after AND/OR operator.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            output.Append(" ");
            output.Append(op);
            output.Append(" ");
        }

        #endregion
    }

    #region Documentation.
    /// <summary>
    /// A search condition specifies the characteristics of rows to be affected by a query or rule. 
    /// It consists of one or more boolean conditions (predicates) which evaluate to TRUE, FALSE, or UNKNOWN.
    /// </summary>
    /// <remarks><para>Specifies the conditions for the rows returned in the result set for a SELECT statement, 
    /// query expression, or subquery. For an UPDATE statement, specifies the rows to be updated. 
    /// For a DELETE statement, specifies the rows to be deleted.</para>
    /// <para>Search condition is a combination of one or more predicates linked by the logical operators "AND" and "OR".</para>
    /// </remarks>
    /// <example>
    /// The following example fetches employees from the given city, but only those who were hired after 
    /// the specified date or those whose hire date hasn't been entered.
    /// <code>
    /// public DataTable FetchEmployeesByCityAndHireDate(string cityName, DateTime hireDate)
    /// {
    /// 	//	SELECT Employees.*
    /// 	//	FROM Employees
    /// 	//	WHERE
    /// 	//		-- Employees from the given city.
    /// 	//		City = @cityName
    /// 	//		AND 
    /// 	//		(
    /// 	//			-- Hired after the given date or hire date not entered.
    /// 	//			HireDate IS NULL
    /// 	//			OR
    /// 	//			HireDate > @hireDate
    /// 	//		)
    /// 	
    /// 	EmployeesMeta employees = new EmployeesMeta();
    /// 	SelectStatement select = new SelectStatement(employees);
    /// 	select.SelectList.Add(employees.Columns);
    /// 
    ///		// City = @cityName
    /// 	select.Where.Add(PredicateFactory.Compare(employees.City, "=", cityName));
    /// 	
    /// 	// HireDate IS NULL  OR  HireDate > @hireDate
    /// 	SearchCondition hiredAfterOrUnknown = new SearchCondition(PredicateFactory.IsNull(employees.HireDate));
    /// 	hiredAfterOrUnknown.Or(PredicateFactory.Compare(employees.HireDate, ">", hireDate));
    /// 	
    /// 	select.Where.And(hiredAfterOrUnknown);
    /// 	
    /// 	return select.Execute();
    /// }
    /// </code>
    /// <b>Note:</b> the <c>hiredAfterOrUnknown</c> sub-condition is created to ensure that the OR expression
    /// is rendered inside parenthesis.
    /// </example>
    #endregion
    [Serializable]
    public sealed class SearchCondition : IRenderSql
    {
        #region Members.

        /// <summary>
        /// Contains a combination of predicates, condition operators and sub conditions.
        /// A condition operator (AND/OR) is placed between to predicates/sub conditions.
        /// All elements of the list implement IRenderSql interface.	
        /// Only PredicateBase or ConditionOpearator objects are in the list.
        /// </summary>
        List<IRenderSql> predicateExpression = new List<IRenderSql>();

        #endregion

        #region Contructors.

        /// <summary>
        /// Initializes a new instance of the SearchCondition class.
        /// </summary>
        public SearchCondition()
        {
        }

        /// <summary>
        /// Initializes a new instance of the SearchCondition class.
        /// </summary>
        /// <param name="firstPredicate">First/initial predicate added to the search condition.</param>
        public SearchCondition(PredicateBase firstPredicate)
        {
            Add(firstPredicate);
        }

        /// <summary>
        /// Initializes a new instance of the SearchCondition class.
        /// </summary>
        /// <param name="field">Database field which is compared to the provided value.</param>
        /// <param name="value">Value to which the field is compared. Nullable.</param>
        public SearchCondition(IDbColumn field, object value)
        {
            Add(field, value);
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets the value which indicates whether the search condition is empty.
        /// </summary>
        public bool IsEmpty
        {
            get
            {
                if (this.predicateExpression.Count == 0)
                    return true;
                else
                    return false;
            }
        }

        #endregion

        #region AND.

        /// <summary>
        /// Adds a boolean condition to the SearchCondition. 
        /// If the SearchCondition is not empty then the expression is added with an "AND" operator.
        /// Essentially, same as <see cref="And(PredicateBase)"/>.
        /// </summary>
        /// <param name="predicate">Predicate to be added to the SearchCondition. <b>Null</b> is not allowed.</param>
        public void Add(PredicateBase predicate)
        {
            And(predicate);
        }

        /// <summary>
        /// Adds a <see cref="ComparePredicate"/> or <see cref="IsNullPredicate"/> to the SearchCondition 
        /// with an "AND" operator.	If the SearchCondition is empty the "AND" operator is not added. 
        /// Essentially, same as <see cref="And(IDbColumn, object)"/>.
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="value">Value to which the field is compared. Nullable.</param>
        public void Add(IDbColumn field, object value)
        {
            And(field, value);
        }

        /// <summary>
        /// Adds a boolean condition to the SearchCondition. 
        /// If the SearchCondition is not empty then the expression is added with an "AND" operator.
        /// Essentially, same as <see cref="And(SearchCondition)"/>.
        /// </summary>
        /// <param name="subCondition">Sub-condition to be added to the SearchCondition. 
        /// <b>Null</b> and empty search conditions are not allowed.</param>
        public void Add(SearchCondition subCondition)
        {
            And(subCondition);
        }

        /// <summary>
        /// Adds a boolean condition to the SearchCondition with an "AND" operator.	
        /// If the SearchCondition is empty the "AND" operator is not added. 
        /// </summary>
        /// <param name="predicate">Predicate to be added to the SearchCondition. <b>Null</b> is not allowed.</param>
        public void And(PredicateBase predicate)
        {
            if (predicate == null)
                throw new ArgumentNullException("predicate", Messages.SearchCondition_PredicateMayNotBeNull);

            if (!this.IsEmpty)
                this.predicateExpression.Add(ConditionOperator.And);

            this.predicateExpression.Add(predicate);
        }

        /// <summary>
        /// Adds a <see cref="ComparePredicate"/> or <see cref="IsNullPredicate"/> to the SearchCondition 
        /// with an "AND" operator.	If the SearchCondition is empty the "AND" operator is not added. 
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="value">Value to which the field is compared. Nullable.</param>
        public void And(IDbColumn field, object value)
        {
            PredicateBase predicate;
            if (value != null)
                predicate = PredicateFactory.Compare(field, ComparisonOperator.Equal, value);
            else
                predicate = PredicateFactory.IsNull(field);

            And(predicate);
        }

        /// <summary>
        /// Adds a boolean condition to the SearchCondition with an "AND" operator.	
        /// If the SearchCondition is empty the "AND" operator is not added. 
        /// </summary>
        /// <param name="subCondition">Sub-condition to be added to the SearchCondition. 
        /// <b>Null</b> and empty search conditions are not allowed.</param>
        public void And(SearchCondition subCondition)
        {
            if (subCondition == null || subCondition.IsEmpty)
                throw new ArgumentNullException("subCondition", Messages.SearchCondition_SubConditionMayNotBeNull);

            if (!this.IsEmpty)
                this.predicateExpression.Add(ConditionOperator.And);

            this.predicateExpression.Add(subCondition);
        }

        /// <summary>
        /// Adds a <b>"not equal"</b> or <b>"is not null"</b> predicate to the SearchCondition 
        /// with an "AND" operator.	If the SearchCondition is empty the "AND" operator is not added. 
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="value">Value to which the field is compared. Nullable.</param>
        public void AndNot(IDbColumn field, object value)
        {
            PredicateBase predicate;
            if (value != null)
                predicate = PredicateFactory.Compare(field, ComparisonOperator.NotEqual, value);
            else
                predicate = PredicateFactory.IsNull(field, /*is not null*/ true);

            And(predicate);
        }

        #endregion

        #region OR.

        /// <summary>
        /// Adds a boolean condition to the SearchCondition with an "OR" operator.	
        /// If the SearchCondition is empty the "OR" operator is not added.	
        /// </summary>
        /// <param name="predicate">Predicate to be added to the SearchCondition. <b>Null</b> is not allowed.</param>
        public void Or(PredicateBase predicate)
        {
            if (predicate == null)
                throw new ArgumentNullException("predicate", Messages.SearchCondition_PredicateMayNotBeNull);

            if (!this.IsEmpty)
                this.predicateExpression.Add(ConditionOperator.Or);

            this.predicateExpression.Add(predicate);
        }

        /// <summary>
        /// Adds a <see cref="ComparePredicate"/> or <see cref="IsNullPredicate"/> to the SearchCondition 
        /// with an "OR" operator.	If the SearchCondition is empty the "OR" operator is not added. 
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="value">Value to which the field is compared. Nullable.</param>
        public void Or(IDbColumn field, object value)
        {
            PredicateBase predicate;
            if (value != null)
                predicate = PredicateFactory.Compare(field, ComparisonOperator.Equal, value);
            else
                predicate = PredicateFactory.IsNull(field);

            Or(predicate);
        }

        /// <summary>
        /// Adds a boolean condition to the SearchCondition with an "OR" operator.	
        /// If the SearchCondition is empty the "OR" operator is not added.	
        /// </summary>
        /// <param name="subCondition">Sub-condition to be added to the SearchCondition. 
        /// <b>Null</b> and empty search conditions are not allowed.</param>
        public void Or(SearchCondition subCondition)
        {
            if (subCondition == null || subCondition.IsEmpty)
                throw new ArgumentNullException("subCondition", Messages.SearchCondition_SubConditionMayNotBeNull);

            if (!this.IsEmpty)
                this.predicateExpression.Add(ConditionOperator.Or);

            this.predicateExpression.Add(subCondition);
        }

        #endregion

        #region Render.

        /// <summary>
        /// Renders search condition as SQL statement.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            // Enclose search condition in parentheses if it contains more than one object (might contain OR operators).
            if (this.predicateExpression.Count > 1)
                output.Append("(");

            // Only PredicateBase or ConditionOpearator objects are in the list.
            foreach (IRenderSql expressionElement in this.predicateExpression)
                expressionElement.Render(dbms, output, parameters);

            if (this.predicateExpression.Count > 1)
                output.Append(")");
        }

        #endregion        

        #region IsNullOrEmpty.

        /// <summary>
        /// Indicates whether the specified search condition is empty or a <n>null</n> reference.
        /// </summary>
        /// <param name="value">A <see cref="SearchCondition"/> reference.</param>
        /// <returns><b>true</b> if the value parameter is empty or a <n>null</n> reference; otherwise, <b>false</b>.</returns>
        public static bool IsNullOrEmpty(SearchCondition value)
        {
            bool isNullOrEmpty = (value == null) || (value.IsEmpty);
            return isNullOrEmpty;
        }

        #endregion
    }
}